package de.jmda.fx.node;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;

import org.apache.log4j.Logger;

import de.jmda.fx.node.behaviour.BehaviourHighlighting;
import de.jmda.fx.node.behaviour.BehaviourHighlightingOnMouseEntered;
import de.jmda.fx.node.behaviour.BehaviourHighlightingOnMouseExited;
import de.jmda.fx.node.behaviour.BehaviourMouseDragging;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingConstraint;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingEvents;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingStart;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingStop;
import de.jmda.fx.node.behaviour.Draggable;
import de.jmda.fx.node.behaviour.DraggingCalculator;
import de.jmda.fx.node.behaviour.DraggingCalculatorDefault;
import de.jmda.fx.node.behaviour.DraggingData;
import de.jmda.fx.node.behaviour.Highlightable;
import de.jmda.util.gui.awt.graphics.RelationEndpoint.Style;

public class ArrowOrthogonal
		extends Polygon
		implements Highlightable, Draggable
{
	private final static Logger LOGGER = Logger.getLogger(ArrowOrthogonal.class);

	/**
	 * head of arrow, often touches (anchor point of) another shape
	 * <p>
	 * Will be updated if arrow moves.
	 */
	private ObjectProperty<Point2D> head = new SimpleObjectProperty<Point2D>(new Point2D(0,0));
	public Point2D getHead() { return head.get(); }
	protected void setHead(Point2D head) { this.head.set(head); }
	public ObjectProperty<Point2D> headProperty() { return head; }

	/**
	 * tail of arrow, often works as anchor point for other shapes
	 * <p>
	 * Will be updated if arrow moves.
	 */
	private ObjectProperty<Point2D> tail = new SimpleObjectProperty<Point2D>(new Point2D(0,0));	
	public Point2D getTail() { return tail.get(); }
	protected void setTail(Point2D tail) { this.tail.set(tail); }
	public ObjectProperty<Point2D> tailProperty() { return tail; }

	private double length;           // constructor injection
	private Style style;             // constructor injection
	private boolean dashed;          // constructor injection
	private double headSize;         // constructor injection
	private Orientation orientation; // constructor injection
	
//	private Point2D offset;

	public ArrowOrthogonal(
			Point2D head,
			double length,
			Style style,
			boolean dashed,
			double headSize,
			Orientation orientation)
	{
		super();
		
		this.head.set(head);

		this.length = length;
		this.style = style;
		this.dashed = dashed;
		this.headSize = headSize;
		this.orientation = orientation;
		
		init();

//		offset = tail.get();

		boundsInParentProperty().addListener(new HeadTailPointUpdater<Bounds>());
	}

	@Override
	public void activateDragging()
	{
		BehaviourMouseDraggingEvents behaviourMouseDraggingEvents =
				new BehaviourMouseDraggingEvents();

		// TODO make this work WITHOUT LAYOUT but with HEAD only!!!!!
		// TODO have a look at Line!!!!!
		new BehaviourMouseDraggingStart(this, behaviourMouseDraggingEvents);
		new BehaviourMouseDraggingStop( this, behaviourMouseDraggingEvents);
		new BehaviourMouseDragging(     this, behaviourMouseDraggingEvents);
	}

	@Override
  public void deactivateDragging()
  {
		LOGGER.warn("not (yet) implemented");
  }

	public void activateHighlighting()
	{
		new BehaviourHighlightingOnMouseEntered<MouseEvent>(this, this);
		new BehaviourHighlightingOnMouseExited<MouseEvent>(this, this);
	}

	@Override
  public void deactivateHighlighting()
  {
		LOGGER.warn("not (yet) implemented");
  }

	@Override
  public void highlightingOn()
  {
		setStrokeWidth(getStrokeWidth() + BehaviourHighlighting.STROKE_WIDTH_DIFF);
  }

	@Override
  public void highlightingOff()
  {
		setStrokeWidth(getStrokeWidth() - BehaviourHighlighting.STROKE_WIDTH_DIFF);
  }

	public Orientation getOrientation()
  {
  	return orientation;
  }

	public void setLength(double length)
	{
		this.length = length;
		LOGGER.debug
		(
				  "before init: "
				+ Util.boundsAsString(getBoundsInParent()) + " "
//				+ coordinatesAsString(this)
		);
		init();
		LOGGER.debug
		(
				  "after  init: "
				+ Util.boundsAsString(getBoundsInParent()) + " "
//				+ coordinatesAsString(this)
		);
	}

	public double getLength()
  {
	  return length;
  }

	/**
	 * Computes all coordinates of the polygon, sets {@link #tailProperty()} and
	 * registers listeners on {@link #headProperty()} and {@link #tailProperty()}.
	 */
	private void init()
  {
		// shortcut
		ObservableList<Double> points = getPoints();

		// reset polygon
		points.clear();

		setStroke(Color.BLACK);
//		setStrokeWidth(1);
//		getStyleClass().add("typeshape");

		if (dashed)
		{
			setStrokeDashOffset(5);
		}

		double x = head.get().getX();
		double y = head.get().getY();

		if (orientation == Orientation.DOWN)
		{
			if (style == Style.FILLED)
			{
				points.setAll
				(
						x           , y           ,
						x - headSize, y - headSize,
						x + headSize, y - headSize,
						x           , y           ,
						x           , y - length
				);
			}
			else if (style == Style.TRANSPARENT)
			{
				points.setAll
				(
						x           , y - headSize,
						x - headSize, y - headSize,
						x           , y           ,
						x + headSize, y - headSize,
						x           , y - headSize,
						x           , y - length
				);
			}
			else if (style == Style.OPEN)
			{
				points.setAll
				(
						x           , y           ,
						x - headSize, y - headSize,
						x           , y           ,
						x + headSize, y - headSize,
						x           , y           ,
						x           , y - length
				);
			}
			else if (style == Style.NONE)
			{
				points.setAll
				(
						x, y,
						x, y - length
				);
			}
//			setTail(new Point2D(x, y - length));
			tail.set(new Point2D(x, y - length));
		}
		else if (orientation == Orientation.UP)
		{
			if (style == Style.FILLED)
			{
				points.setAll
				(
						x           , y           ,
						x - headSize, y + headSize,
						x + headSize, y + headSize,
						x           , y           ,
						x           , y + length
				);
			}
			else if (style == Style.TRANSPARENT)
			{
				points.setAll
				(
						x           , y + headSize,
						x - headSize, y + headSize,
						x           , y           ,
						x + headSize, y + headSize,
						x           , y + headSize,
						x           , y + length
				);
			}
			else if (style == Style.OPEN)
			{
				points.setAll
				(
						x           , y           ,
						x - headSize, y + headSize,
						x           , y           ,
						x + headSize, y + headSize,
						x           , y           ,
						x           , y + length
				);
			}
			else if (style == Style.NONE)
			{
				points.setAll
				(
						x, y,
						x, y + length
				);
			}
//			setTail(new Point2D(x, y + length));
			tail.set(new Point2D(x, y + length));
		}
		else if (orientation == Orientation.LEFT)
		{
			if (style == Style.FILLED)
			{
				points.setAll
				(
						x           , y           ,
						x + headSize, y - headSize,
						x + headSize, y + headSize,
						x           , y           ,
						x + length  , y
				);
			}
			else if (style == Style.TRANSPARENT)
			{
				points.setAll
				(
						x + headSize, y           ,
						x + headSize, y - headSize,
						x           , y           ,
						x + headSize, y + headSize,
						x + headSize, y           ,
						x + length  , y
				);
			}
			else if (style == Style.OPEN)
			{
				points.setAll
				(
						x           , y           ,
						x + headSize, y - headSize,
						x           , y           ,
						x + headSize, y + headSize,
						x           , y           ,
						x + length  , y
				);
			}
			else if (style == Style.NONE)
			{
				points.setAll
				(
						x         , y,
						x + length, y
				);
			}
//			setTail(new Point2D(x + length, y));
			tail.set(new Point2D(x + length, y));
		}
		else if (orientation == Orientation.RIGHT)
		{
			if (style == Style.FILLED)
			{
				points.setAll
				(
						x           , y           ,
						x - headSize, y - headSize,
						x - headSize, y + headSize,
						x           , y           ,
						x - length  , y
				);
			}
			else if (style == Style.TRANSPARENT)
			{
				points.setAll
				(
						x - headSize, y           ,
						x - headSize, y - headSize,
						x           , y           ,
						x - headSize, y + headSize,
						x - headSize, y           ,
						x - length  , y
				);
			}
			else if (style == Style.OPEN)
			{
				points.setAll
				(
						x           , y           ,
						x - headSize, y - headSize,
						x           , y           ,
						x - headSize, y + headSize,
						x           , y           ,
						x - length  , y
				);
			}
			else if (style == Style.NONE)
			{
				points.setAll
				(
						x         , y,
						x - length, y
				);
			}
//			setTail(new Point2D(x - length, y));
			tail.set(new Point2D(x - length, y));
		}

		if (style == Style.FILLED)
		{
			setFill(Color.BLACK);
		}
		if (style == Style.TRANSPARENT)
		{
			setFill(Color.TRANSPARENT);
		}
	}

	public static String coordinatesAsString(ArrowOrthogonal arrow)
	{
		return
				"coords " +
				"[" +
						"arrow " + Util.layoutAsString(arrow)           + " " +
						"head "  + Util.pointAsString(arrow.head.get()) + " " +
						"tail "  + Util.pointAsString(arrow.tail.get()) +
				"]";
	}

	/**
	 * orthogonal orientation
	 */
	public enum Orientation
	{
		LEFT,
		RIGHT,
		UP,
		DOWN;
		
		public boolean isHorizontal()
		{
			return (this == LEFT) || (this == RIGHT);
		}
		
		public boolean isVertical()
		{
			return !isHorizontal();
		}
	}

	private class HeadTailPointUpdater<T extends Bounds> implements
	    ChangeListener<Bounds>
	{
		@Override
		public void changed(ObservableValue<? extends Bounds> observable,
		    Bounds oldValue, Bounds newValue)
		{
			String before =
					"before update " +
							Util.boundsAsString(ArrowOrthogonal.this.getBoundsInParent());
			double deltaX = newValue.getMinX() - oldValue.getMinX();
			double deltaY = newValue.getMinY() - oldValue.getMinY();
			Point2D newHead = new Point2D(head.get().getX() + deltaX, head.get().getY() + deltaY);
			Point2D newTail = new Point2D(tail.get().getX() + deltaX, tail.get().getY() + deltaY);
//			LOGGER.debug(
//					coordinatesAsString(ArrowOrthogonal.this) +
//					"\nold " + boundsAsString(oldValue) +
//					"\nnew " + boundsAsString(newValue));
//			head.set(
//					new Point2D(
//							layoutXProperty().get() + offset.getX(),
//							layoutYProperty().get() + offset.getY()));
//			tail.set(
//					new Point2D(
//							layoutXProperty().get() + offset.getX(),
//							layoutYProperty().get() + offset.getY()));
//
			if (ArrowOrthogonal.this.orientation == Orientation.UP)
			{
				LOGGER.debug("before " + coordinatesAsString(ArrowOrthogonal.this));
				headProperty().set(
						new Point2D(
								head.get().getX() + deltaX,
								head.get().getY() + deltaY));
				tailProperty().set(
						new Point2D(
								tail.get().getX() + deltaX,
								tail.get().getX() + deltaY));
				LOGGER.debug("after  " + coordinatesAsString(ArrowOrthogonal.this));
			}
		}
	}

	/**
	 * Adds attribute to super class that stores {@link ArrowOrthogonal} instance.
	 */
	public class BehaviourMouseDraggingData extends BehaviourMouseDraggingEvents
	{
		/**
		 * constructor injection
		 */
		private ArrowOrthogonal arrow;

		public BehaviourMouseDraggingData(ArrowOrthogonal arrow)
    {
	    super();
	    this.arrow = arrow;
    }
	}

	/**
	 * Propagates {@link ArrowOrthogonal} instance to {@link
	 * BehaviourMouseDraggingData} instance.
	 */
	public class BehaviourMouseDraggingStartArrowOrthogonal
			extends BehaviourMouseDraggingStart
	{
		public BehaviourMouseDraggingStartArrowOrthogonal(ArrowOrthogonal arrow)
		{
			super(arrow, new BehaviourMouseDraggingData(arrow));
		}
	}

	public class BehaviourMouseDraggingArrowOrthogonal
			extends BehaviourMouseDragging
	{
		/**
		 * constructor injection
		 */
		protected BehaviourMouseDraggingData behaviourMouseDraggingData;

		public BehaviourMouseDraggingArrowOrthogonal(
				BehaviourMouseDraggingData behaviourMouseDraggingData)
    {
	    super(behaviourMouseDraggingData.arrow, behaviourMouseDraggingData);

			this.behaviourMouseDraggingData = behaviourMouseDraggingData;
    }

		@Override
	  public void showBehaviour(MouseEvent mouseEventCurrent)
	  {
			// store current mouse event coordinates
			double mouseEventCurrentX = mouseEventCurrent.getSceneX();
			double mouseEventCurrentY = mouseEventCurrent.getSceneY();
			
			// shortcut to most recent mouse coordinates
			double mouseEventMostRecentX = behaviourMouseDraggingData.getMouseDraggingMostRecent().getSceneX();
			double mouseEventMostRecentY = behaviourMouseDraggingData.getMouseDraggingMostRecent().getSceneY();
			
			// shortcut to arrow
			ArrowOrthogonal arrow = behaviourMouseDraggingData.arrow;
			
			BehaviourMouseDraggingConstraint lineDraggingConstraint =
					new BehaviourMouseDraggingConstraint(arrow);
			
			DraggingData lineDraggingDataRequest =
					new DraggingData(
							mouseEventCurrentX - mouseEventMostRecentX,
							mouseEventCurrentY - mouseEventMostRecentY);
			
			DraggingCalculator lineDraggingCalculator =
					new DraggingCalculatorDefault(
							behaviourMouseDraggingData.arrow, lineDraggingDataRequest, lineDraggingConstraint);
			
			DraggingData lineDraggingData = lineDraggingCalculator.calculate();

			double headX = arrow.head.get().getX() + lineDraggingData.getDragX();
			double headY = arrow.head.get().getY() + lineDraggingData.getDragY();
			double tailX = arrow.tail.get().getX() + lineDraggingData.getDragX();
			double tailY = arrow.tail.get().getY() + lineDraggingData.getDragY();

			// set arrow head and tail to new coordinates
			arrow.head.set(new Point2D(headX, headY));
			arrow.tail.set(new Point2D(tailX, tailY));
			
			behaviourMouseDraggingData.setMouseDragging(mouseEventCurrent);
	  }
	}
}